﻿#region Copyright & License

//
// Author: Ian Davis <ian.f.davis@gmail.com> Copyright (c) 2007-2008, Ian Davs
//
// Portions of this software were developed for NUnit. See NOTICE.txt for more
// information. 
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
// the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.
//

#endregion

#region Using Directives

using System;
using System.Collections;
using System.Collections.Generic;
using Ensurance.Constraints.Collection;
using Ensurance.Constraints.Comparison;
using Ensurance.Constraints.Prefix;
using Ensurance.Constraints.String;
using Ensurance.Constraints.Type;

#endregion

namespace Ensurance.Constraints
{
    /// <summary>
    /// ConstraintBuilder is used to resolve the Not and All properties, which
    /// serve as prefix operators for constraints. With the addition of an
    /// operand stack, And and Or could be supported, but we have left them out
    /// in favor of a simpler, more type-safe implementation. Use the &amp; and
    /// | operator overloads to combine constraints.
    /// </summary>
    public class ConstraintBuilder<T>
    {
        private Stack<string> _opnds = new Stack<string>();
        private Stack<Op> _ops = new Stack<Op>();

        /// <summary>
        /// Implicitly convert ConstraintBuilder to an actual Constraint at the
        /// point where the syntax demands it.
        /// </summary>
        /// <param name="builder"></param>
        /// <returns></returns>
        public static implicit operator Constraint<T>(ConstraintBuilder<T> builder)
        {
            return builder.Resolve();
        }

        #region Constraints Without Arguments

        /// <summary>
        /// Resolves the chain of constraints using EqualConstraint(null) as
        /// base.
        /// </summary>
        public Constraint<T> Null
        {
            get { return Resolve(new EqualConstraint<T>(default(T))); }
        }

        /// <summary>
        /// Resolves the chain of constraints using EqualConstraint(true) as
        /// base.
        /// </summary>
        public Constraint<T> True
        {
            get { return Resolve(new EqualConstraint<T>(default(T))); }
        }

        /// <summary>
        /// Resolves the chain of constraints using EqualConstraint(false) as
        /// base.
        /// </summary>
        public Constraint<T> False
        {
            get { return Resolve(new EqualConstraint<T>(default(T))); }
        }

        /// <summary>
        /// Resolves the chain of constraints using Is.NaN as base.
        /// </summary>
        public Constraint<double> NaN
        {
            get { return Resolve(new EqualConstraint<double>(double.NaN)); }
        }

        /// <summary>
        /// Resolves the chain of constraints using Is.Empty as base.
        /// </summary>
        public Constraint<T> Empty
        {
            get { return Resolve(new EmptyConstraint<T>()); }
        }

        /// <summary>
        /// Resolves the chain of constraints using Is.Unique as base.
        /// </summary>
        public Constraint<ICollection> Unique
        {
            get { return Resolve(new UniqueItemsConstraint<T>()); }
        }

        #endregion

        #region Constraints with an expected value

        #region Equality and Identity

        /// <summary>
        /// Resolves the chain of constraints using an EqualConstraint as base.
        /// </summary>
        public Constraint<T> EqualTo(T expected)
        {
            return Resolve(new EqualConstraint<T>(expected));
        }

        /// <summary>
        /// Resolves the chain of constraints using a SameAsConstraint as base.
        /// </summary>
        public Constraint<T> SameAs(T expected)
        {
            return Resolve(new SameAsConstraint<T>(expected));
        }

        #endregion

        #region Comparison Constraints

        /// <summary>
        /// Resolves the chain of constraints using a LessThanConstraint as
        /// base.
        /// </summary>
        public Constraint<T> LessThan(IComparable expected)
        {
            return Resolve(new LessThanConstraint<T>(expected));
        }

        /// <summary>
        /// Resolves the chain of constraints using a GreaterThanConstraint as
        /// base.
        /// </summary>
        public Constraint<T> GreaterThan(IComparable expected)
        {
            return Resolve(new GreaterThanConstraint<T>(expected));
        }

        /// <summary>
        /// Resolves the chain of constraints using a LessThanOrEqualConstraint
        /// as base.
        /// </summary>
        public Constraint<T> LessThanOrEqualTo(IComparable expected)
        {
            return Resolve(new LessThanOrEqualConstraint<T>(expected));
        }

        /// <summary>
        /// Resolves the chain of constraints using a LessThanOrEqualConstraint
        /// as base.
        /// </summary>
        public Constraint<T> AtMost(IComparable expected)
        {
            return Resolve(new LessThanOrEqualConstraint<T>(expected));
        }

        /// <summary>
        /// Resolves the chain of constraints using a
        /// GreaterThanOrEqualConstraint as base.
        /// </summary>
        public Constraint<T> GreaterThanOrEqualTo(IComparable expected)
        {
            return Resolve(new GreaterThanOrEqualConstraint<T>(expected));
        }

        /// <summary>
        /// Resolves the chain of constraints using a
        /// GreaterThanOrEqualConstraint as base.
        /// </summary>
        public Constraint<T> AtLeast(IComparable expected)
        {
            return Resolve(new GreaterThanOrEqualConstraint<T>(expected));
        }

        #endregion

        #region Type Constraints

        /// <summary>
        /// Resolves the chain of constraints using an ExactTypeConstraint as
        /// base.
        /// </summary>
        public Constraint<System.Type> TypeOf(System.Type expectedType)
        {
            return Resolve(new ExactTypeConstraint(expectedType));
        }

        /// <summary>
        /// Resolves the chain of constraints using an InstanceOfTypeConstraint
        /// as base.
        /// </summary>
        public Constraint<System.Type> InstanceOfType(System.Type expectedType)
        {
            return Resolve(new InstanceOfTypeConstraint(expectedType));
        }

        /// <summary>
        /// Resolves the chain of constraints using an AssignableFromConstraint
        /// as base.
        /// </summary>
        public Constraint<System.Type> AssignableFrom(System.Type expectedType)
        {
            return Resolve(new AssignableFromConstraint(expectedType));
        }

        #endregion

        #region Containing Constraint

        /// <summary>
        /// Resolves the chain of constraints using a ContainsConstraint as
        /// base. This constraint will, in turn, make use of the appropriate
        /// second-level constraint, depending on the type of the actual
        /// argument.
        /// </summary>
        public Constraint<ICollection> Contains(T expected)
        {
            return Resolve(new CollectionContainsConstraint<T>(expected));
        }

        /// <summary>
        /// Resolves the chain of constraints using a ContainsConstraint as
        /// base. This constraint will, in turn, make use of the appropriate
        /// second-level constraint, depending on the type of the actual
        /// argument.
        /// </summary>
        public Constraint<string> Contains(string expected)
        {
            return Resolve( new StringContainsConstraint( expected as string ) );
        }

        /// <summary>
        /// Resolves the chain of constraints using a 
        /// CollectionContainsConstraint as base.
        /// </summary>
        /// <param name="expected">The expected object</param>
        public Constraint<ICollection> Member(T expected)
        {
            return Resolve(new CollectionContainsConstraint<T>(expected));
        }

        #endregion

        #region String Constraints

        /// <summary>
        /// Resolves the chain of constraints using a StartsWithConstraint as
        /// base.
        /// </summary>
        public Constraint<string> StartsWith(string substring)
        {
            return Resolve(new StartsWithConstraint(substring));
        }

        /// <summary>
        /// Resolves the chain of constraints using a StringEndingConstraint as
        /// base.
        /// </summary>
        public Constraint<string> EndsWith(string substring)
        {
            return Resolve(new EndsWithConstraint(substring));
        }

        /// <summary>
        /// Resolves the chain of constraints using a StringMatchingConstraint
        /// as base.
        /// </summary>
        public Constraint<string> Matches(string pattern)
        {
            return Resolve(new RegexConstraint(pattern));
        }

        #endregion

        #region Collection Constraints

        /// <summary>
        /// Resolves the chain of constraints using a
        /// CollectionEquivalentConstraint as base.
        /// </summary>
        public Constraint<ICollection> EquivalentTo(ICollection expected)
        {
            return Resolve(new CollectionEquivalentConstraint<T>(expected));
        }

        /// <summary>
        /// Resolves the chain of constraints using a
        /// CollectionContainingConstraint as base.
        /// </summary>
        public Constraint<ICollection> CollectionContaining(T expected)
        {
            return Resolve(new CollectionContainsConstraint<T>(expected));
        }

        /// <summary>
        /// Resolves the chain of constraints using a CollectionSubsetConstraint
        /// as base.
        /// </summary>
        public Constraint<ICollection> SubsetOf(ICollection expected)
        {
            return Resolve(new CollectionSubsetConstraint<ICollection>(expected));
        }

        #endregion

        #region Property Constraints

        /// <summary>
        /// Resolves the chain of constraints using a PropertyConstraint as base
        /// </summary>
        public Constraint<T> Property<T>(string name, T expected)
        {
            return Resolve(new PropertyConstraint<T>(name, new EqualConstraint<T>(expected)));
        }

        /// <summary>
        /// Resolves the chain of constraints using a PropertyConstraint on
        /// Length as base
        /// </summary>
        /// <param name="length"></param>
        /// <returns></returns>
        public Constraint<int> Length(int length)
        {
            return Property("Length", length);
        }

        /// <summary>
        /// Resolves the chain of constraints using a PropertyConstraint on
        /// Length as base
        /// </summary>
        /// <param name="count"></param>
        /// <returns></returns>
        public Constraint<int> Count(int count)
        {
            return Property("Count", count);
        }

        #endregion

        #endregion

        #region Prefix Operators

        /// <summary>
        /// Modifies the ConstraintBuilder by pushing a Not operator on the
        /// stack.
        /// </summary>
        public ConstraintBuilder<T> Not
        {
            get
            {
                _ops.Push(Op.Not);
                return this;
            }
        }

        /// <summary>
        /// Modifies the ConstraintBuilder by pushing a Not operator on the
        /// stack.
        /// </summary>
        public ConstraintBuilder<T> No
        {
            get
            {
                _ops.Push(Op.Not);
                return this;
            }
        }

        /// <summary>
        /// Modifies the ConstraintBuilder by pushing an All operator on the
        /// stack.
        /// </summary>
        public ConstraintBuilder<T> All
        {
            get
            {
                _ops.Push(Op.All);
                return this;
            }
        }

        /// <summary>
        /// Modifies the ConstraintBuilder by pushing a Some operator on the
        /// stack.
        /// </summary>
        public ConstraintBuilder<T> Some
        {
            get
            {
                _ops.Push(Op.Some);
                return this;
            }
        }

        /// <summary>
        /// Modifies the constraint builder by pushing All and Not operators on
        /// the stack
        /// </summary>
        public ConstraintBuilder<T> None
        {
            get
            {
                _ops.Push(Op.None);
                return this;
            }
        }

        /// <summary>
        /// Modifies the ConstraintBuilder by pushing a Prop operator on the ops
        /// stack and the name of the property on the opnds stack.
        /// </summary>
        /// <param name="name"></param>
        /// <returns></returns>
        public ConstraintBuilder<T> Property(string name)
        {
            _ops.Push(Op.Prop);
            _opnds.Push(name);
            return this;
        }

        #endregion

        #region Helper Methods

        /// <summary>
        /// Resolve a constraint that has been recognized by applying any
        /// pending operators and returning the resulting Constraint.
        /// </summary>
        /// <returns>A constraint that incorporates all pending operators</returns>
        private Constraint<T> Resolve<T>(Constraint<T> constraint)
        {
            while (_ops.Count > 0)
            {
                switch (_ops.Pop())
                {
                    case Op.Not:
                        constraint = new NotConstraint<T>(constraint);
                        break;
                    case Op.All:
                        constraint = new AllItemsConstraint<T>(constraint);
                        break;
                    case Op.Some:
                        constraint = new SomeItemsConstraint<T>(constraint);
                        break;
                    case Op.None:
                        constraint = new NoItemConstraint<T>(constraint);
                        break;
                    case Op.Prop:
                        constraint = new PropertyConstraint<T>(_opnds.Pop(), constraint);
                        break;
                }
            }

            return constraint;
        }

        private Constraint<T> Resolve()
        {
            return Resolve(default(Constraint<T>));
        }

        #endregion

        #region Nested type: Op

        private enum Op
        {
            Not,
            All,
            Some,
            None,
            Prop,
        }

        #endregion
    }
}
